package com.itheima.stock.service.impl;

import com.google.common.collect.Lists;
import com.itheima.stock.constant.ParseType;
import com.itheima.stock.face.StockCacheFace;
import com.itheima.stock.mapper.*;
import com.itheima.stock.pojo.entity.StockBlockRtInfo;
import com.itheima.stock.pojo.entity.StockMarketIndexInfo;
import com.itheima.stock.pojo.entity.StockOuterMarketIndexInfo;
import com.itheima.stock.pojo.entity.StockRtInfo;
import com.itheima.stock.pojo.vo.StockInfoConfig;
import com.itheima.stock.service.StockTimerTaskService;
import com.itheima.stock.utils.DateTimeUtil;
import com.itheima.stock.utils.IdWorker;
import com.itheima.stock.utils.ParserStockInfoUtil;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;

import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service("stockTimerTaskService")
@Slf4j
public class StockTimerTaskServiceImpl implements StockTimerTaskService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private StockInfoConfig stockInfoConfig;

    @Autowired
    private IdWorker idWorker;

    @Autowired
    private StockMarketIndexInfoMapper stockMarketIndexInfoMapper;

    @Autowired
    private ParserStockInfoUtil parserStockInfoUtil;

    @Autowired
    private StockBusinessMapper stockBusinessMapper;

    @Autowired
    private StockRtInfoMapper stockRtInfoMapper;

    @Autowired
    private StockBlockRtInfoMapper stockBlockRtInfoMapper;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Autowired
    private StockOuterMarketIndexInfoMapper stockOuterMarketIndexInfoMapper;

    @Autowired
    private StockCacheFace stockCacheFace;

    /**
     * 必须保证该对象无状态
     */
    private HttpEntity<Object> entity;

    @Override
    public void getInnerMarketInfo() {
        // 阶段1：采集数据
        //1.定义采集的url接口
        String url = stockInfoConfig.getMarketUrl() + String.join(",", stockInfoConfig.getInner());

        //2.调用restTemplate采集数据
//        //2.1 组装请求头
//        HttpHeaders headers = new HttpHeaders();
//        //必须填写，否则数据采集不到
//        headers.add("Referer", "https://finance.sina.com.cn/stock/");
//        headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36");
//        //2.2 组装请求对象
//        HttpEntity<Object> entity = new HttpEntity<>(headers);

        //2.3 resetTemplate发起请求
        //String resString = restTemplate.postForObject(url, entity, String.class);
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        int statusCodeValue = responseEntity.getStatusCodeValue();
        if (statusCodeValue != 200) {
            // 当前请求失败
            log.error("当前时间点: {}, 采集数据失败, http状态码: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), statusCodeValue);
            // 其他：发送邮件、企业微信、钉钉等给相关运营人员提醒
            return;
        }
        // 获取js格式数据
        String resString = responseEntity.getBody();
        log.info("当前时间点: {}, 采集原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), resString);

        // 阶段2：java正则解析原始数据
        //3.数据解析（重要）
//        var hq_str_sh000001="上证指数,3267.8103,3283.4261,3236.6951,3290.2561,3236.4791,0,0,402626660,398081845473,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2022-04-07,15:01:09,00,";
//        var hq_str_sz399001="深证成指,12101.371,12172.911,11972.023,12205.097,11971.334,0.000,0.000,47857870369,524892592190.995,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,0,0.000,2022-04-07,15:00:03,00";
        String reg = "var hq_str_(.+)=\"(.+)\";";
        //编译表达式,获取编译对象
        Pattern pattern = Pattern.compile(reg);
        //匹配字符串
        Matcher matcher = pattern.matcher(resString);
        ArrayList<StockMarketIndexInfo> list = new ArrayList<>();
        //判断是否有匹配的数值
        while (matcher.find()) {
            //1.获取大盘的code
            String marketCode = matcher.group(1);
            //2.获取其它信息，字符串以逗号间隔
            String otherInfo = matcher.group(2);
            //以逗号切割字符串，形成数组
            String[] splitArr = otherInfo.split(",");
            //大盘名称
            String marketName = splitArr[0];
            //获取当前大盘的开盘点数
            BigDecimal openPoint = new BigDecimal(splitArr[1]);
            //前收盘点
            BigDecimal preClosePoint = new BigDecimal(splitArr[2]);
            //获取大盘的当前点数
            BigDecimal curPoint = new BigDecimal(splitArr[3]);
            //获取大盘最高点
            BigDecimal maxPoint = new BigDecimal(splitArr[4]);
            //获取大盘的最低点
            BigDecimal minPoint = new BigDecimal(splitArr[5]);
            //获取成交量
            Long tradeAmt = Long.valueOf(splitArr[8]);
            //获取成交金额
            BigDecimal tradeVol = new BigDecimal(splitArr[9]);
            //时间
            Date curTime = DateTimeUtil.getDateTimeWithoutSecond(splitArr[30] + " " + splitArr[31]).toDate();
            // 阶段3：组装entity对象
            StockMarketIndexInfo info = StockMarketIndexInfo.builder()
                    .id(idWorker.nextId())
                    .marketCode(marketCode)
                    .marketName(marketName)
                    .curPoint(curPoint)
                    .openPoint(openPoint)
                    .preClosePoint(preClosePoint)
                    .maxPoint(maxPoint)
                    .minPoint(minPoint)
                    .tradeVolume(tradeVol)
                    .tradeAmount(tradeAmt)
                    .curTime(curTime)
                    .build();
            //收集封装的对象，方便批量插入
            list.add(info);
        }
        log.info("采集的当前大盘数据：{}", list);

        // 阶段4：批量插入
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        //完成批量插入功能
        int count = stockMarketIndexInfoMapper.insertBatch(list);
        if (count > 0) {
            log.info("批量插入了：{}条数据", count);
            log.info("当前时间点: {}, 插入成功原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);

            //通知后台终端刷新本地缓存，发送的日期数据是告知对方当前更新的股票数据所在时间点
            //发送日期对象，接收方通过接受的日期与当前日期对比，能判断数据延迟的时长，用于维护通知处理
            rabbitTemplate.convertAndSend("stockExchange", "inner.market", new Date());
        } else {
            log.info("当前时间点: {}, 插入失败原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), list);
        }
    }

    /**
     * 批量获取股票分时数据详情信息
     * http://hq.sinajs.cn/list=sz000002,sh600015
     */
    @Override
    public void getStockRtIndex() {
        //1.批量获取股票ID集合
        List<String> allStockCodes = stockCacheFace.getAllStockCodeWithPredix();

        /*List<String> allStockCodes = stockBusinessMapper.getAllStockCodes();
        //计算出符合sina命名规范的股票id数据
        allStockCodes = allStockCodes.stream().map(id -> {
            return id.startsWith("6") ? "sh" + id : "sz" + id;
        }).collect(Collectors.toList());*/
        //设置公共请求头对象
        //设置请求头数据
        //HttpHeaders headers = new HttpHeaders();
        //headers.add("Referer", "https://finance.sina.com.cn/stock/");
        //headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36");
        //HttpEntity<String> entity = new HttpEntity<>(headers);

        long startTime = System.currentTimeMillis();

        //一次性查询过多，我们将需要查询的数据先进行分片处理，每次最多查询20条股票数据
        //核心思路：将大的集合切分成若干小集合，分批次拉取数据
        Lists.partition(allStockCodes, 20).forEach(list -> {
            //原始方案
            /*//拼接股票url地址
            String stockUrl = stockInfoConfig.getMarketUrl() + String.join(",", list);
            //获取响应数据
            ResponseEntity<String> responseEntity = restTemplate.exchange(stockUrl, HttpMethod.GET, entity, String.class);
            int statusCodeValue = responseEntity.getStatusCodeValue();
            if (statusCodeValue != 200) {
                // 当前请求失败
                log.error("当前时间点: {}, 采集数据失败, http状态码: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), statusCodeValue);
                // 其他：发送邮件、企业微信、钉钉等给相关运营人员提醒
                return;
            }
            // 获取js格式数据
            String result = responseEntity.getBody();

            List<StockRtInfo> infos = parserStockInfoUtil.parser4StockOrMarketInfo(result, ParseType.ASHARE);
            log.info("采集个股数据: {}", infos.size());

            //批量保存采集的个股数据
            int count = stockRtInfoMapper.insertBatch(infos);
            if (count > 0) {
                log.info("批量插入了：{}条数据", count);
                log.info("当前时间点: {}, 插入成功原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), infos);
            } else {
                log.info("当前时间点: {}, 插入失败原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), infos);
            }*/

            //方案1：原始方案采集各股数据时将集合分片，然后分批次串行采集数据，效率不高，存在较高的采集延迟 - 引入多线程
            //代码问题：1.每次来任务，就创建一个线程，复用性差 2.如果多线程使用不当，造成CPU竞争激烈，导致频繁的上下文切换，导致程序性能降低
            /*new Thread(() -> {
                String stockUrl = stockInfoConfig.getMarketUrl() + String.join(",", list);
                //获取响应数据
                ResponseEntity<String> responseEntity = restTemplate.exchange(stockUrl, HttpMethod.GET, entity, String.class);
                int statusCodeValue = responseEntity.getStatusCodeValue();
                if (statusCodeValue != 200) {
                    // 当前请求失败
                    log.error("当前时间点: {}, 采集数据失败, http状态码: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), statusCodeValue);
                    // 其他：发送邮件、企业微信、钉钉等给相关运营人员提醒
                    return;
                }
                // 获取js格式数据
                String result = responseEntity.getBody();

                List<StockRtInfo> infos = parserStockInfoUtil.parser4StockOrMarketInfo(result, ParseType.ASHARE);
                log.info("采集个股数据: {}", infos.size());

                //批量保存采集的个股数据
                int count = stockRtInfoMapper.insertBatch(infos);
                if (count > 0) {
                    log.info("批量插入了：{}条数据", count);
                    log.info("当前时间点: {}, 插入成功原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), infos);
                } else {
                    log.info("当前时间点: {}, 插入失败原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), infos);
                }
            }).start();*/

            //方案2：引入线程池
            //每个分片的数据开启一个线程异步执行任务
            threadPoolTaskExecutor.execute(() -> {
                String stockUrl = stockInfoConfig.getMarketUrl() + String.join(",", list);
                //获取响应数据
                ResponseEntity<String> responseEntity = restTemplate.exchange(stockUrl, HttpMethod.GET, entity, String.class);
                int statusCodeValue = responseEntity.getStatusCodeValue();
                if (statusCodeValue != 200) {
                    // 当前请求失败
                    log.error("当前时间点: {}, 采集数据失败, http状态码: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), statusCodeValue);
                    // 其他：发送邮件、企业微信、钉钉等给相关运营人员提醒
                    return;
                }
                // 获取js格式数据
                String result = responseEntity.getBody();

                List<StockRtInfo> infos = parserStockInfoUtil.parser4StockOrMarketInfo(result, ParseType.ASHARE);
                log.info("采集个股数据: {}", infos.size());

                //批量保存采集的个股数据
                int count = stockRtInfoMapper.insertBatch(infos);
                if (count > 0) {
                    log.info("批量插入了：{}条数据", count);
                    log.info("当前时间点: {}, 插入成功原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), infos);
                } else {
                    log.info("当前时间点: {}, 插入失败原始的数据: {}", DateTime.now().toString("yyyy-MM-dd HH:mm:ss"), infos);
                }
            });
        });

        long endTime = System.currentTimeMillis();
        log.info("take time: {}", endTime - startTime);
    }

    /**
     * 获取板块实时数据
     * http://vip.stock.finance.sina.com.cn/q/view/newSinaHy.php
     */
    @Override
    public void getStockSectorRtIndex() {
        //发送板块数据请求
        String result = restTemplate.getForObject(stockInfoConfig.getBlockUrl(), String.class);
        //响应结果转板块集合数据
        List<StockBlockRtInfo> infos = parserStockInfoUtil.parse4StockBlock(result);
        log.info("板块数据量：{}", infos.size());
        //数据分片保存到数据库下 行业板块类目大概50个，可每小时查询一次即可
        Lists.partition(infos, 20).forEach(list -> {
            threadPoolTaskExecutor.execute(() -> {
                //20个一组，批量插入
                stockBlockRtInfoMapper.insertBatch(list);
            });
        });
    }

    /**
     * 获取国外大盘的实时数据信息
     */
    @Override
    public void getOuterMarketInfo() {
        String url = stockInfoConfig.getMarketUrl() + String.join(",", stockInfoConfig.getOuter());
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
        int statusCodeValue = responseEntity.getStatusCodeValue();
        if (statusCodeValue != 200) {
            log.error("采集国外大盘数据失败,状态码：{}", statusCodeValue);
            return;
        }
        String jsData = responseEntity.getBody();
        List<StockOuterMarketIndexInfo> entities = parserStockInfoUtil.parser4StockOrMarketInfo(jsData, ParseType.OUTER);
        log.info("采集的当前国外大盘数据：{}", entities);
        int count = stockOuterMarketIndexInfoMapper.insertBatch(entities);
        if (count > 0) {
            rabbitTemplate.convertAndSend("stockExchange", "outer.market", new Date());
            log.info("插入国外大盘数据成功！批量插入了：{}条数据", count);
        } else {
            log.info("插入国外大盘数据失败！");
        }
    }

    /**
     * bean生命周期的初始化回调方法
     */
    @PostConstruct
    public void init() {
        //2.1 组装请求头
        HttpHeaders headers = new HttpHeaders();
        //必须填写，否则数据采集不到
        headers.add("Referer", "https://finance.sina.com.cn/stock/");
        headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36");
        //2.2 组装请求对象
        entity = new HttpEntity<>(headers);
    }
} 